Skip to content

[#851] Port Linux/Windows instrument-driver menu items to Qt - #881

Merged
eoyilmaz merged 5 commits into
developfrom
851-qt-instrument-driver-menu-items
Jul 18, 2026
Merged

[#851] Port Linux/Windows instrument-driver menu items to Qt#881
eoyilmaz merged 5 commits into
developfrom
851-qt-instrument-driver-menu-items

Conversation

@eoyilmaz

Copy link
Copy Markdown
Owner

Summary

  • Ports the 4 platform-conditional Instrument-submenu items missing from the Qt MainWindow (issue [Qt] Linux/Windows instrument-driver menu items missing (install/uninstall udev rules & drivers) #851): Linux udev-rules configuration file install/uninstall, and Windows instrument driver install/uninstall, gated the same way wx gates them (sys.platform / TEST flag), including their confirmation dialogs and the authenticate()-driven elevated-install flow.
  • Fixes Worker.authenticate() unconditionally constructing a BetterWindowDisabler(), which touches wx.GetTopLevelWindows() and crashes natively on Linux/GTK when no wx.App is running, a path the new Linux menu items can now reach under Qt-only operation.
  • Mocks the sound system app-wide in the test suite (tests/conftest.py), so beep.wav/camera_shutter.wav/intro_new.wav no longer play during test runs.

Test plan

  • pytest tests/test_ui_main_window.py (23 new tests covering menu presence/absence, dialog flows, and both handlers) — 535 passed
  • Full suite (pytest tests/ -n auto) — 2511 passed, 21 skipped
  • Verified the Linux code path against a real (non-macOS) Linux environment via Docker: constructed a real MainWindow() and confirmed the udev-conf menu actions appear/behave correctly

eoyilmaz added 5 commits July 18, 2026 14:31
…rsion to 3.10.0.dev49

Add the 4 platform-conditional Instrument-submenu items that were missing
from the Qt MainWindow: Linux udev-rules config install/uninstall and
Windows driver install/uninstall, gated the same way wx gates them
(`sys.platform`/`TEST` flag), including their confirmation dialogs and
`authenticate()`-driven elevated-install flow.

Also fix `Worker.authenticate()` unconditionally constructing a
`BetterWindowDisabler()`, which touches `wx.GetTopLevelWindows()` and
crashes the process natively on Linux/GTK when no `wx.App` is running
(the new Linux code path can hit this under Qt-only operation); guard it
with `wx.GetApp() is not None`.
Every "instrument connected" / "measurement taken" / startup chime
(`worker.py`'s measurement/commit sounds, `display_cal.py`'s startup
sound, `ui/startup.py`'s `play_startup_sound()`, the untethered window,
...) goes through `audio.Sound(path).safe_play()`. All of those call
sites reference it as `audio.Sound(...)`, so replacing the class
attribute with the module's own no-op `DummySound` in `conftest.py`,
before any other DisplayCAL module runs, silences beep.wav/
camera_shutter.wav/intro_new.wav for the whole test session instead of
needing a per-file monkeypatch.
…touse fixture

test_confirm_bpc_choice_turn_on_accept_persists_bpc drives the real
_confirm_black_point_correction_choice() "accept" path, which persists
calibration.black_point_correction = 1.0, and never restores it. Left
leaked, a later test's freshly-constructed window initializes its
black-point-correction control from that value, satisfying one of
measurement_mode_ctrl_handler()'s guards for showing the (here unmocked)
BPC choice QMessageBox, which then blocks forever on a headless CI
runner. Same "config leak wakes a real modal" trap the fixture already
guards against for 3 other keys, just missing this one; caught via a
real hang in test_measurement_mode_ctrl_populates_and_persists on
Linux + Python 3.12 CI (PR #881, run 29646328469).
…atform sys.platform spoof crash

test_instrument_menu_matches_wx_xrc_order, test_instrument_conf_and_
driver_actions_absent_on_macos_by_default, and test_update_instrument_
conf_menu_state_noop_without_action all asserted "no platform-conditional
instrument items" using the shared `window` fixture, which builds against
whatever the real CI host's sys.platform actually is. That only holds on
macOS; on real Linux/Windows CI hosts the #851 items now legitimately
exist, and both failed for real (PR #881 run 29646328469).

Pinning them to "darwin" via `monkeypatch.setattr(mw.sys, "platform",
...)` + `mw.MainWindow()` (the pattern the existing
test_instrument_conf_actions_present_on_linux already used) isn't safe
either: `mw.sys` is the real global `sys` module, so the patch also
reaches the `Worker()` -> `ThreadAbort()` -> `mp.Event()` call
`MainWindow.__init__` makes internally, and CPython's own
`multiprocessing` resource-tracker dispatches its spawn path off that
same real `sys.platform`. Spoofing "linux"/"darwin" while the real host
is Windows makes it try to `import _posixsubprocess`, which doesn't
exist there, crashing the whole worker instead of failing the assertion
(seen for real on Windows CI, same run).

Added `_build_window_with_platform()`: construct and enumerate the
`Worker` first (under the real, unspoofed platform), *then* spoof
`sys.platform` and hand the already-built worker to
`MainWindow(worker=...)`, so `__init__` adopts it instead of
constructing (and spoof-crashing on) its own. All 4 platform-spoofing
tests in this section now go through it.
…own sys reference

The previous fix (construct Worker() before spoofing mw.sys.platform)
only patched over one specific crash. mw.sys is the real, single global
sys module, so monkeypatch.setattr(mw.sys, "platform", ...) doesn't just
affect _build_tools_menu()'s own gate -- it reaches every other module's
sys.platform reads for the rest of the test too. Confirmed a second,
different crash from the same root cause on Linux CI: DisplayCAL.config
only imports LIBRARY/LIBRARY_HOME at module-import time when the *real*
host is "darwin" (config.py:26-37), so spoofing "darwin" on a real Linux
host makes colorimeter_correction.py's data-file lookup raise
AttributeError: module 'DisplayCAL.config' has no attribute 'LIBRARY'
the moment MainWindow build reaches the CCXX catalog (PR #881, run
29647026132).

Replace the spoof mechanism: instead of mutating the shared sys module,
replace the *name* `sys` inside main_window's own module namespace with
a _FakeSysModule that overrides only `.platform` and delegates every
other attribute (argv, exit, getwindowsversion) to the real sys module.
This confines the spoof to exactly main_window.py's own platform checks,
so config.py, colorimeter_correction.py, and CPython's own
multiprocessing dispatch all keep seeing the real, unmodified
sys.platform. Simpler too: no longer needs to pre-construct and adopt a
Worker to dodge the mp.Event() crash, since the real sys module (and
therefore multiprocessing's dispatch) is never touched.
@eoyilmaz
eoyilmaz merged commit cabd329 into develop Jul 18, 2026
29 of 30 checks passed
@eoyilmaz
eoyilmaz deleted the 851-qt-instrument-driver-menu-items branch July 18, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant